package daybook.model.transaction;

import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.*;

// DateSet包含startDate和endDate, 和所有不晚于endDate的所有日期startDate + n * temporalUnit
public class DateSet extends AbstractSet<LocalDate> implements NavigableSet<LocalDate> {
    private final LocalDate startDate;
    private final LocalDate endDate;
    private final TemporalUnit temporalUnit;

    public DateSet(LocalDate startDate, LocalDate endDate, TemporalUnit temporalUnit) {
        Objects.requireNonNull(startDate);
        Objects.requireNonNull(endDate);
        if (!temporalUnit.isDateBased()) {
            throw new IllegalArgumentException("temporalUnit must be date-based");
        }

        if (temporalUnit.getDuration().compareTo(ChronoUnit.YEARS.getDuration()) > 0) {
            throw new IllegalArgumentException("temporalUnit's duration must not be greater than YEARS");
        }

        if (startDate.compareTo(endDate) > 0) {
            throw new IllegalArgumentException("startDate > endDate");
        }

        this.startDate = startDate;
        this.endDate = endDate;
        this.temporalUnit = temporalUnit;
    }

    @Override
    public boolean contains(Object o) {
        if (o instanceof LocalDate) {
            final LocalDate localDate = (LocalDate) o;
            if (localDate.isBefore(startDate) || localDate.isAfter(endDate)) {
                return false;
            }
            if (localDate.isEqual(startDate) || localDate.isEqual(endDate)) {
                return true;
            }

            final long n = startDate.until(localDate, temporalUnit);
            final LocalDate date = startDate.plus(n, temporalUnit);
            return date.isEqual(localDate);
        } else {
            return false;
        }
    }

    // result = lower(date);
    // result == null || (contains(result) && result < date && for all d < date, result >= d)
    @Override
    public LocalDate lower(LocalDate localDate) {
        if (localDate.compareTo(startDate) <= 0) {
            return null;
        }
        if (localDate.isAfter(endDate)) {
            return endDate;
        }

        // (startDate, endDate]
        final long n = startDate.until(localDate.minusDays(1L), temporalUnit);
        return startDate.plus(n, temporalUnit);
    }

    // result = floor(date);
    // result == null || (contains(result) && result <= date && for all d <= date, result >= d)
    @Override
    public LocalDate floor(LocalDate localDate) {
        if (localDate.isBefore(startDate)) {
            return null;
        }
        if (localDate.compareTo(endDate) >= 0) {
            return endDate;
        }

        // [startDate, endDate)
        final long n = startDate.until(localDate, temporalUnit);
        return startDate.plus(n, temporalUnit);
    }

    // result = ceiling(date);
    // result == null || (contains(result) && result >= date && for all d >= date, result <= d)
    @Override
    public LocalDate ceiling(LocalDate localDate) {
        if (localDate.compareTo(startDate) <= 0) {
            return startDate;
        }
        if (localDate.isAfter(endDate)) {
            return null;
        }

        // (startDate, endDate]
        final long n = startDate.until(localDate.minusDays(1L), temporalUnit);
        final LocalDate result = startDate.plus(n + 1, temporalUnit);
        return result.isAfter(endDate) ? endDate : result;
    }

    // result = higher(date);
    // result == null || (contains(result) && result > date && for all d > date, result <= d)
    @Override
    public LocalDate higher(LocalDate localDate) {
        if (localDate.isBefore(startDate)) {
            return startDate;
        }
        if (localDate.compareTo(endDate) >= 0) {
            return null;
        }

        // [startDate, endDate)
        final long n = startDate.until(localDate, temporalUnit);
        final LocalDate result = startDate.plus(n + 1, temporalUnit);
        return result.isAfter(endDate) ? endDate : result;
    }

    @Override
    public LocalDate pollFirst() {
        throw new UnsupportedOperationException();
    }

    @Override
    public LocalDate pollLast() {
        throw new UnsupportedOperationException();
    }

    @Override
    public Iterator<LocalDate> iterator() {
        return new Iterator<LocalDate>() {
            private LocalDate date = startDate;

            @Override
            public boolean hasNext() {
                return date != null;
            }

            @Override
            public LocalDate next() {
                if (date == null) {
                    throw new NoSuchElementException();
                }

                final LocalDate result = date;
                try {
                    date = date.plus(1, temporalUnit);
                    if (date.isAfter(endDate)) {
                        date = result.isBefore(endDate) ? endDate : null;
                    }
                } catch (DateTimeException e) {
                    date = result.isBefore(endDate) ? endDate : null;
                }

                return result;
            }
        };
    }

    @Override
    public NavigableSet<LocalDate> descendingSet() {
        throw new UnsupportedOperationException();
    }

    @Override
    public Iterator<LocalDate> descendingIterator() {
        throw new UnsupportedOperationException();
    }

    @Override
    public NavigableSet<LocalDate> subSet(LocalDate fromDate, boolean fromInclusive, LocalDate toDate, boolean toInclusive) {
        if (fromDate.isAfter(toDate)) {
            throw new IllegalArgumentException("fromDate is after toDate");
        }

        final LocalDate newStartDate = fromInclusive ? ceiling(fromDate) : higher(fromDate);
        if (newStartDate == null) {
            return Collections.emptyNavigableSet();
        }

        final LocalDate newEndDate = toInclusive ? floor(toDate) : lower(toDate);
        if (newEndDate == null) {
            return Collections.emptyNavigableSet();
        }

        if (newStartDate.isAfter(newEndDate)) {
            return Collections.emptyNavigableSet();
        }

        return new DateSet(newStartDate, newEndDate, temporalUnit);
    }

    @Override
    public NavigableSet<LocalDate> headSet(LocalDate toDate, boolean inclusive) {
        return subSet(startDate, true, toDate, inclusive);
    }

    @Override
    public NavigableSet<LocalDate> tailSet(LocalDate fromDate, boolean inclusive) {
        return subSet(fromDate, inclusive, endDate, true);
    }

    @Override
    public Comparator<? super LocalDate> comparator() {
        return Comparator.naturalOrder();
    }

    @Override
    public SortedSet<LocalDate> subSet(LocalDate fromDate, LocalDate toDate) {
        return subSet(fromDate, true, toDate, false);
    }

    @Override
    public SortedSet<LocalDate> headSet(LocalDate toDate) {
        return headSet(toDate, false);
    }

    @Override
    public SortedSet<LocalDate> tailSet(LocalDate fromDate) {
        return tailSet(fromDate, true);
    }

    @Override
    public LocalDate first() {
        return startDate;
    }

    @Override
    public LocalDate last() {
        return endDate;
    }

    @Override
    public int size() {
        // 计算从startDate(不包含)到endDate(包含)有多少个temporalUnit
        long size = startDate.until(endDate, temporalUnit);
        // 如果最后一个日期(startDate.plus(size, temporalUnit))和endDate相同
        // 则再算上startDate,给size加1;否则要算上startDate和endDate,给size加2
        size += startDate.plus(size, temporalUnit).equals(endDate) ? 1L : 2L;
        return  (size > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) size;
    }

    @Override
    public String toString() {
        return "DateSet{" +
                "startDate=" + startDate +
                ", endDate=" + endDate +
                ", temporalUnit=" + temporalUnit +
                '}';
    }
}
